﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MultiplexingGestureController.cs" company="arvystate.net">
//   arvystate.net
// </copyright>
// <summary>
//   Gesture controller only sends one same command from multiple recognizers
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace PresentationAid.Lib.GestureControllers
{
    using System;
    using System.Collections.Generic;

    using Kinect.Toolbox;

    using Microsoft.Kinect;

    using NLog;

    using PresentationAid.Lib.Enums;
    using PresentationAid.Lib.Gestures;

    /// <summary>
    /// Gesture controller only sends one same command from multiple recognizers
    /// </summary>
    public class MultiplexingGestureController : IGestureController
    {
        #region Static Fields

        /// <summary>
        /// The logger.
        /// </summary>
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

        #endregion

        // Holds gesture history
        #region Fields

        /// <summary>
        /// The _gesture commands.
        /// </summary>
        private readonly Dictionary<int, List<GestureCommand>> gestureCommands;

        /// <summary>
        /// The _gesture controllers.
        /// </summary>
        private readonly List<IGestureController> gestureControllers;

        /// <summary>
        /// The _last recognitions.
        /// </summary>
        private readonly Dictionary<int, DateTime> lastRecognitions;

        /// <summary>
        /// The _skeletons.
        /// </summary>
        private Skeleton[] skeletons;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="MultiplexingGestureController"/> class.
        /// </summary>
        public MultiplexingGestureController()
        {
            Logger.Trace("Init: MultiplexingGestureController - BlockSize: 1000");

            this.gestureControllers = new List<IGestureController>();
            this.gestureCommands = new Dictionary<int, List<GestureCommand>>();
            this.lastRecognitions = new Dictionary<int, DateTime>();

            this.BlockSize = 1000;
        }

        #endregion

        #region Public Events

        /// <summary>
        /// The gesture recognized.
        /// </summary>
        public event GestureEventHandler GestureRecognized;

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the gesture block size in milliseconds
        /// </summary>
        public int BlockSize { get; set; }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        /// Adds another gesture controller to multiplexer
        /// </summary>
        /// <param name="controller">
        /// Gesture controller to add
        /// </param>
        public void AddGestureController(IGestureController controller)
        {
            Logger.Debug("Method: AddGestureController - GestureController: {0}", controller.GetType().Name);

            // Subscribe to controller gesture event
            controller.GestureRecognized += this.OnGestureRecognized;

            // Add controller to gesture controllers
            this.gestureControllers.Add(controller);
        }

        /// <summary>
        /// Recognizes gestures in current SkeletonFrame
        /// </summary>
        /// <param name="frame">
        /// SkeletonFrame with skeletons
        /// </param>
        public void Recognize(SkeletonFrame frame)
        {
            // Check skeletons if we have anything to track really, we are looking for a skeleton with both
            // hands visible to make sure we do not detect false positives.
            frame.GetSkeletons(ref this.skeletons);

            bool shouldRecognize = false;

            foreach (Skeleton skeleton in this.skeletons)
            {
                if (skeleton.TrackingState != SkeletonTrackingState.Tracked)
                {
                    continue;
                }

                // Check for hands
                int handCount = 0;

                foreach (Joint joint in skeleton.Joints)
                {
                    if ((joint.JointType == JointType.HandLeft) && (joint.TrackingState == JointTrackingState.Tracked))
                    {
                        handCount++;
                    }
                    else if ((joint.JointType == JointType.HandRight)
                             && (joint.TrackingState == JointTrackingState.Tracked))
                    {
                        handCount++;
                    }
                }

                if (handCount < 2)
                {
                    continue;
                }

                shouldRecognize = true;

                break;
            }

            // Have a skeleton with two hands
            if (!shouldRecognize)
            {
                return;
            }

            // Execute each recognition
            foreach (IGestureController gestureController in this.gestureControllers)
            {
                gestureController.Recognize(frame);
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gesture was recognized by one of the child controllers.
        /// </summary>
        /// <param name="sender">
        /// Sender of the gesture recognized event
        /// </param>
        /// <param name="e">
        /// Event arguments
        /// </param>
        private void OnGestureRecognized(object sender, GestureEventArgs e)
        {
            Logger.Info(
                "Event: GestureRecognized - Gesture: {0}, Sender: {1}", e.Command.ToString(), sender.GetType().Name);

            // Check if the recognized skeleton is in our gesture history
            Logger.Trace(
                "EventDetail: GestureRecognized - GestureHistory: {0}", this.gestureCommands.ContainsKey(e.TrackingId));

            if (this.gestureCommands.ContainsKey(e.TrackingId))
            {
                // Check if already tracked skeleton has already moved in last second
                if ((DateTime.Now - this.lastRecognitions[e.TrackingId]).TotalMilliseconds > this.BlockSize)
                {
                    Logger.Trace(
                        "EventDetail: GestureRecognized - LastSecondMove: {0}, BlockSize: {1}", 
                        (DateTime.Now - this.lastRecognitions[e.TrackingId]).TotalMilliseconds, 
                        this.BlockSize);

                    // Need to check empty block, otherwise were sending command straight
                    if (this.gestureCommands[e.TrackingId].Count > 0)
                    {
                        Logger.Trace(
                            "EventDetail: GestureRecognized - BlockCheck, TrackingId: {0}, Count: {1}", 
                            e.TrackingId, 
                            this.gestureCommands[e.TrackingId].Count);

                        // We're out of block, need to send command, so we shall first find out which one to send.
                        // StartKinect by counting the commands, we do not expect more than 1 - 10 commands here,
                        // so the time needed for this is minimal.
                        Dictionary<GestureCommand, int> commands = new Dictionary<GestureCommand, int>();

                        foreach (GestureCommand command in this.gestureCommands[e.TrackingId])
                        {
                            if (commands.ContainsKey(command))
                            {
                                commands[command]++;
                            }
                            else
                            {
                                commands.Add(command, 1);
                            }
                        }

                        // Find out the command to send, that had the most recognition
                        GestureCommand maxCommand = this.gestureCommands[e.TrackingId][0];
                        int maxCommandCount = commands[maxCommand];

                        foreach (KeyValuePair<GestureCommand, int> pair in commands)
                        {
                            if (pair.Value <= maxCommandCount)
                            {
                                continue;
                            }

                            maxCommand = pair.Key;
                            maxCommandCount = pair.Value;
                        }

                        Logger.Trace(
                            "EventDetail: GestureRecognized - MaxCommand: {0}, TrackingId: {0}", 
                            maxCommand.ToString(), 
                            e.TrackingId);

                        // Fire event
                        this.GestureRecognized(this, new GestureEventArgs(maxCommand, e.TrackingId));
                    }
                    else
                    {
                        Logger.Trace(
                            "EventDetail: GestureRecognized - EmptyBlock: {0}, TrackingId: {0}", 
                            e.Command.ToString(), 
                            e.TrackingId);

                        this.GestureRecognized(this, e);
                    }

                    this.lastRecognitions[e.TrackingId] = DateTime.Now;
                    this.gestureCommands[e.TrackingId].Clear();
                }
            }
            else
            {
                // The skeleton is not in history, gesture is valid, need to still ensure history will be tracked
                // if he moves again in the same block.
                this.gestureCommands.Add(e.TrackingId, new List<GestureCommand> { e.Command });
                this.lastRecognitions.Add(e.TrackingId, DateTime.Now);
            }

            Logger.Trace("EventDetail: GestureRecognized - CleanTracking");

            // Cleaning tracking ID's that havent moved for more than a second, to ensure we do not keep a memory leak
            List<int> cleanList = new List<int>();

            foreach (KeyValuePair<int, DateTime> pair in this.lastRecognitions)
            {
                if ((DateTime.Now - pair.Value).TotalMilliseconds > this.BlockSize)
                {
                    cleanList.Add(pair.Key);
                }
            }

            foreach (int i in cleanList)
            {
                this.lastRecognitions.Remove(i);
                this.gestureCommands.Remove(i);
            }
        }

        #endregion
    }
}